﻿using System;
using Pyramid.Utility;
namespace Pyramid.IO
{
	/// <summary>
	/// Provides a balanced-ternary wrapper around binary .NET streams.
	/// </summary>
	/// <remarks>
	/// <para>
	/// The BinaryStream wrapper encodes each balanced tryte as an Int16;
	/// because of this, and to ensure that Length & Position properties behave
	/// in a consistent way, trytes are aligned to 16-bit boundaries if the
	/// underlying stream supports Seeks. This may cause the stream wrapper to
	/// skip bytes if the position of the wrapped stream is an odd number when a
	/// read or write is attempted.
	/// </para>
	/// <para>
	/// This will have no effect if both ends of the wrapped stream are
	/// BinaryStreams, as the behaviour is identical in both read and write
	/// directions. However, if binary data is being stored or transmitted
	/// alongside the ternary data, the ternary sections may be preceded by a
	/// padding byte to ensure the 16-bit boundary alignment. Mixing binary and
	/// ternary data within a seekable stream is, frankly, a bad idea.
	/// </para>
	/// </remarks>
	public class BinaryStream
		: Stream
	{
		private System.IO.Stream binaryStream;

		public BinaryStream(System.IO.Stream binaryStream)
		{
			this.binaryStream = binaryStream;
		}

		public override bool CanRead
		{
			get
			{
				return this.binaryStream.CanRead;
			}
		}

		public override bool CanSeek
		{
			get
			{
				return this.binaryStream.CanSeek;
			}
		}

		public override bool CanWrite
		{
			get
			{
				return this.binaryStream.CanWrite;
			}
		}

		public override void Flush()
		{
			this.binaryStream.Flush();
		}

		public override long Length
		{
			get
			{
				return this.binaryStream.Length / 2;
			}
		}

		public override long Position
		{
			get
			{
				return (this.binaryStream.Position + 1) / 2;
			}
			set
			{
				this.binaryStream.Position = value * 2;
			}
		}

		public override int Read(Tryte[] buffer, int offset, int count)
		{
			Validate.IsLongEnough(buffer, offset + count, "buffer");

			if (CanSeek)
			{
				// If the underlying stream is not aligned to a 16-bit boundary,
				// skip a byte before beginning the read.
				if (this.binaryStream.Position % 2 == 1)
				{
					this.binaryStream.Position++;
				}
			}

			int byteCount = count * 2;

			var byteBuffer = new byte[byteCount];

			int actualByteCount = this.binaryStream.Read(byteBuffer, 0, byteCount);

			for (int byteIndex = 0; (byteIndex + 1) < byteCount; byteIndex += 2)
			{
				buffer[offset + (byteIndex / 2)] = (Tryte)BitConverter.ToInt16(byteBuffer, byteIndex);
			}

			return actualByteCount / 2;
		}

		public override long Seek(long offset, System.IO.SeekOrigin origin)
		{
			return this.binaryStream.Seek(offset * 2, origin);
		}

		public override void SetLength(long value)
		{
			this.binaryStream.SetLength(value * 2);
		}

		public override void Write(Tryte[] buffer, int offset, int count)
		{
			Validate.IsLongEnough(buffer, offset + count, "buffer");

			if (CanSeek)
			{
				// If the underlying stream is not aligned to a 16-bit boundary,
				// skip a byte before beginning the write.
				if (this.binaryStream.Position % 2 == 1)
				{
					this.binaryStream.Position++;
				}
			}

			int byteCount = count * 2;

			var byteBuffer = new byte[byteCount];

			for (int byteIndex = 0; (byteIndex + 1) < byteCount; byteIndex += 2)
			{
				var tryteBytes = BitConverter.GetBytes((short)buffer[offset + (byteIndex / 2)]);

				Array.Copy(tryteBytes, 0, byteBuffer, byteIndex, 2);
			}

			this.binaryStream.Write(byteBuffer, 0, byteCount);
		}
	}
}
